// Copyright (c) Microsoft Corporation.
// 
// Author: Chris King
// Extensively Modified: Vance Morrison
// Date: 4/2007

using System;
using System.Collections.Generic;
using System.Diagnostics;
using EnvDTE;
using EnvDTE80;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.VisualStudio.TextManager.Interop;
using VSAddins;

/// <summary>
/// The CodeBrowser class allows you to nativigate by looking up code symbols in Visual Studio. The main
/// routines are code:CodeBrowser.GoToSymbolInSelectionScope and code:CodeBrowser.GoOut
/// </summary>
public static class CodeBrowser
{
    /// <summary>
    /// Implements the Alt-F12 functionality (goto symbol), however instead of presenting the user with a
    /// list of options, simply go to the first one.   It does use the scope from the selection point in
    /// the active document to resolve partial names (however fully qualified names will aways work). For
    /// example if the selection point is within the class Foo, then it will find the method M on Foo by
    /// just using the name M rather than Foo.M.  It also looks for 'using' statements to try to find
    /// context that way too (although this is currently broken for C++). 
    /// </summary>
    public static void GoToSymbolInSelectionScope(DTE2 dte, string symbol)
    {
        Debug.Assert(symbol.Length > 0);
        Log.WriteLine("In GoToSymbolInSelectionScope symbol = " + symbol);

        bool foundSymbol = false;
        List<string> scopes = GetScopesForSelection(dte);
        Log.WriteLine("Found " + scopes.Count + " scopes to search for symbol.");
        foreach (string scope in scopes)
        {
            string fullSymbol = (scope.Length == 0) ? symbol : (scope + "." + symbol);
            Log.WriteLine("Looking for symbol: " + symbol + " in scope " + scope);
            if (GotoFullyQualifedSymbol(dte, fullSymbol))
            {
                foundSymbol = true;
                break;
            }
        }
        if (!foundSymbol)
            throw new ApplicationException("Could not find symbol " + symbol);

        // Select the symbol, so it gets highlighted.  
        TextRanges tags = null;
        TextDocument txt = (TextDocument)dte.ActiveDocument.Object("TextDocument");
        TextSelection selection = txt.Selection;
        selection.FindPattern(@"[A-Za-z0-9]*", (int)vsFindOptions.vsFindOptionsRegularExpression, ref tags);

        // C++ goes to the end of the symbol rather than the begining, so account for that
        if (!selection.ActivePoint.GreaterThan(selection.AnchorPoint))
        {
            selection.FindPattern(@"<[A-Za-z0-9]*",
                (int)(vsFindOptions.vsFindOptionsBackwards | vsFindOptions.vsFindOptionsRegularExpression), ref tags);
        }
        Log.WriteLine("Goto symbol succeeded: " + symbol);
    }
    /// <summary>
    /// Goto the fully qualified symbol 'symbol'.  'symbol' can use '.' for namespace separators even if the
    /// target function is C++ (which uses '::' as a separator).  It is NOT context senstive (it does not
    /// care what the active document is or where you are within it.  
    /// </summary>
    public static bool GotoFullyQualifedSymbol(DTE2 dte, string symbol)
    {
        uint index;
        IVsObjectList2 list = FindSymbol(dte, symbol, out index);
        if (list == null)
        {
            Log.WriteLine("GotoFullyQualifedSymbol: could not find " + symbol);
            return false;
        }
        if (list.GoToSource(index, VSOBJGOTOSRCTYPE.GS_DEFINITION) != 0)
            throw new ApplicationException("GoToSource for symbol " + symbol + " failed (intellisence being updated)?");
        Log.WriteLine("GotoSource succeeds for scope " + symbol);
        return true;
    }
    /// <summary>
    /// Go to the next outer scope using the selection point in the active document. If in a method, it goes
    /// to the top of the method, If it is at the top of a method it does to the class, if at the top of a
    /// class it goes to the top of the file. This is useful for finding the comment for the more general
    /// thing the item is a part of.
    /// </summary>
    public static void GoOut(DTE2 dte)
    {
        Log.WriteLine("\r\nIn GoOut");
        TextDocument textDocument = (TextDocument)dte.ActiveDocument.Object("TextDocument");
        TextSelection selection = textDocument.Selection;

        CodeElement codeElement = CodeBrowser.GetCodeElementAtSelection(dte);
        if (codeElement == null)
        {
            Log.WriteLine("Could not get code element, going to top of document.");
            textDocument.Selection.StartOfDocument(false);
            return;
        }

        int originalOffset = selection.AnchorPoint.AbsoluteCharOffset;
        if (codeElement.Kind == vsCMElement.vsCMElementFunction)
        {
            Log.WriteLine("Jumping to top of method.");
            int methodStart = codeElement.GetStartPoint(vsCMPart.vsCMPartNavigate).AbsoluteCharOffset;
            selection.MoveToAbsoluteOffset(methodStart, false);
        }
        int newOffset = selection.AnchorPoint.AbsoluteCharOffset;

        // If we didn't move then jump up to outer nesting
        if (originalOffset == newOffset)
        {
            CodeElement parentSymbol = codeElement.Collection.Parent as CodeElement;
            if (parentSymbol == null || parentSymbol.Kind == vsCMElement.vsCMElementNamespace)
            {
                Log.WriteLine("Jumping to top of file.");
                selection.StartOfDocument(false);
            }
            else
            {
                Log.WriteLine("Jumping to class.");
                CodeBrowser.GotoFullyQualifedSymbol(dte, CodeBrowser.GetScopeForCodeElement(dte, parentSymbol));
            }
        }
    }

    #region private
    /// <summary>
    /// Find all the namespaces that should be searched for the current selection point and return them as a
    /// list.  
    /// </summary>
    private static List<string> GetScopesForSelection(DTE2 dte)
    {
        List<string> scopes = new List<string>();
        try
        {
            ProjectItem projectItem = dte.ActiveDocument.ProjectItem;
            FileCodeModel code = projectItem.FileCodeModel;
            List<CodeImport> usingStatements = new List<CodeImport>();
            TextSelection sel = (TextSelection)dte.ActiveDocument.Selection;
            int absoluteCharOffset = sel.ActivePoint.AbsoluteCharOffset;
            CodeElement codeElementScope = GetScopeFromCharOffset(code.CodeElements, absoluteCharOffset, ref usingStatements);

            while (codeElementScope != null && !(codeElementScope.Kind == vsCMElement.vsCMElementClass ||
                  codeElementScope.Kind == vsCMElement.vsCMElementNamespace ||
                  codeElementScope.Kind == vsCMElement.vsCMElementStruct ||
                  codeElementScope.Kind == vsCMElement.vsCMElementUnion))
                codeElementScope = (CodeElement)codeElementScope.Collection.Parent;

            string symbolScope = GetScopeForCodeElement(dte, codeElementScope);
            if (symbolScope.Length > 0)
            {
                for (; ; )
                {
                    scopes.Add(symbolScope);
                    int dot = symbolScope.LastIndexOf('.');
                    if (dot < 0)
                        break;
                    symbolScope = symbolScope.Substring(0, dot);
                }
            }

            // Add any scopes specified by using statements. 
            foreach (CodeImport usingStatement in usingStatements)
            {
                string scope = usingStatement.Namespace;
                scope = scope.Replace("::", ".");       // For C++ normalize the namespace names.  
                scopes.Add(scope);
            }
        }
        catch (Exception e)
        {
            Log.WriteLine("Error: exception caught while resolving scope: " + e.Message);
        }
        scopes.Add("");     // You always have the global scope. 
        return scopes;
    }
    /// <summary>
    /// Given a code element (position in the source code), find the full name of the class or namespace for this code
    /// element (its scope).   It assumes that it is NOT in a method currently (will throw). 
    /// </summary>
    private static string GetScopeForCodeElement(DTE2 dte, CodeElement code)
    {
        if (code != null && code.Kind == vsCMElement.vsCMElementFunction)
            throw new ArgumentException("Method overloading not supported.");

        string path = "";
        if (code != null)
            path = code.FullName;
        Log.WriteLine("Scope from CodeElement: " + path);
        return path;
    }
    private static CodeElement GetCodeElementAtSelection(DTE2 dte)
    {
        TextSelection sel = (TextSelection)dte.ActiveDocument.Selection;
        int absoluteCharOffset = sel.ActivePoint.AbsoluteCharOffset;
        ProjectItem projectItem = dte.ActiveDocument.ProjectItem;
        FileCodeModel code = projectItem.FileCodeModel;
        List<CodeImport> usingStatements = new List<CodeImport>();
        CodeElement scope = GetScopeFromCharOffset(code.CodeElements, absoluteCharOffset, ref usingStatements);
        return scope;
    }
    private static CodeElement GetScopeFromCharOffset(
        CodeElements codes, int absoluteCharOffset, ref List<CodeImport> usingStatements)
    {
        foreach (CodeElement code in codes)
        {
            vsCMElement kind = code.Kind;
            if (usingStatements != null && kind == vsCMElement.vsCMElementImportStmt)
            {
                usingStatements.Add((CodeImport)code);
                continue;
            }

            if (!(kind == vsCMElement.vsCMElementClass ||
                  kind == vsCMElement.vsCMElementNamespace ||
                  kind == vsCMElement.vsCMElementStruct ||
                  kind == vsCMElement.vsCMElementUnion ||
                  kind == vsCMElement.vsCMElementFunction ||
                  kind == vsCMElement.vsCMElementProperty))
                continue;

            TextPoint start = code.GetStartPoint(vsCMPart.vsCMPartWholeWithAttributes);
            TextPoint end = code.GetEndPoint(vsCMPart.vsCMPartWholeWithAttributes);

            if (start.AbsoluteCharOffset <= absoluteCharOffset && absoluteCharOffset < end.AbsoluteCharOffset)
            {
                CodeElement scope = GetScopeFromCharOffset(code.Children, absoluteCharOffset, ref usingStatements);
                if (scope == null)
                    return code;
                return scope;
            }
        }
        return null;
    }

    #region RawInterfaceToVS
    /// <summary>
    /// #WrappedFindSymbol
    /// Only used by code:GotoFullyQualifedSymbol
    /// 
    /// This version of FindSymbol looks up 'symbol' in all of VS's symbol tables (one for each project?).
    /// Like code:#RawFindSymbol it returns a IVsObjectList2 and an index which can be passed to
    /// code:IVsObjectList2.GoToSource to go to the resulting location. Symbol namespace elements are
    /// separated by '.' and this routine will convert them to the correct target separator (eg :: for C++).
    /// </summary>
    private static IVsObjectList2 FindSymbol(DTE2 dte, string symbol, out uint index)
    {
        index = 0;
        foreach (KeyValuePair<IVsLibrary2, string> pair in Services.GetIVsLibraries(dte))
        {
            IVsLibrary2 library = pair.Key;
            string delimiter = pair.Value;

            if (library == null)
                continue;

            string normSymbol = symbol;
            if (delimiter != ".")
            {
                normSymbol = symbol.Replace(".", delimiter);
                Log.WriteLine("Normalized symbol to target: " + normSymbol);
            }
            VSOBSEARCHCRITERIA2[] search = new VSOBSEARCHCRITERIA2[1];
            search[0].eSrchType = VSOBSEARCHTYPE.SO_ENTIREWORD;
            search[0].grfOptions = (int)_VSOBSEARCHOPTIONS.VSOBSO_CASESENSITIVE;
            search[0].szName = normSymbol;

            IVsObjectList2 list = FindSymbol(library, search, _LIB_LISTTYPE.LLT_MEMBERS, out index);
            if (list != null)
                return list;

            list = FindSymbol(library, search, _LIB_LISTTYPE.LLT_CLASSES, out index);
            if (list != null)
                return list;
        }
        return null;
    }
    /// <summary>
    /// #RawFindSymbol
    /// Only used by code:#WrappedFindSymbol
    /// 
    /// This is the raw Symbol lookup API for Visual Studio. It returns a 'location' as a pair: The
    /// IVsObjectList2, along with 'index'. Index can be passed to the code:IVsObjectList2.GoToSource method
    /// to actually cause VS to move to that symbol. This API needs to be passed the library to look up the
    /// symbol in, which is why we immediate wrap it. see code:#WrappedFindSymbol. It currently returns the
    /// first symbol it finds (does not resolve overload ambiguity).
    /// </summary>
    private static IVsObjectList2 FindSymbol(
        IVsLibrary2 library, VSOBSEARCHCRITERIA2[] search,_LIB_LISTTYPE listType, out uint index)
    {
        index = 0;
        IVsObjectList2 resultList = null;
        _LIB_LISTFLAGS useSearchFilter = _LIB_LISTFLAGS.LLF_USESEARCHFILTER;
        string symbol = search[0].szName;
        library.GetList2((uint)listType, (uint)useSearchFilter, search, out resultList);

        if (resultList != null)
        {
            uint count;
            resultList.GetItemCount(out count);
            for (index = 0; index < count; index++)
            {
                object property;
                _VSOBJLISTELEMPROPID fullnameId = _VSOBJLISTELEMPROPID.VSOBJLISTELEMPROPID_FULLNAME;
                resultList.GetProperty(index, (int)fullnameId, out property);

                string fullname = property as string;
                if (fullname.Contains("("))
                    fullname = fullname.Substring(0, fullname.IndexOf('('));

                if (symbol.Equals(fullname))
                    // #SymbolFound
                    return resultList;
            }
        }
        return null;
    }
    #endregion
    #endregion
}

